package roborally.itemsonboard;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;
import be.kuleuven.cs.som.annotate.Raw;
import roborally.board.Board;
import roborally.factory.PhysicalAmountFactory;
import roborally.supporting.DefaultConstants;
import roborally.supporting.PhysicalAmount;
import roborally.supporting.Unit;
import roborally.supporting.Position;
import roborally.usableitems.ItemWithWeight;
import roborally.usableitems.UsableWithBoard;

/**
 * A repairKit is an EnergyItem that can be used. When a robot uses the repairKit,
 * the energyLimit of the robot will increase.
 * 
 * @invar The boardItem must have a valid position at all times. (repeated)
 * 		| canHaveAsPosition(this.getPosition())
 * @invar The boardItem must have a proper board at all times. (repeated)
 * 		| hasProperBoard()
 * @invar The energyItem must have a valid energy level at all times. (repeated)
 * 		| canHaveAsEnergyLevel(this.getEnergyLevel())
 * @invar The energyItem must have a valid energy limit at all times. (repeated)
 * 		| canHaveAsEnergyLimit(this.getEnergyLimit())
 * @invar If this item is possessed by a robot, then the board must be null. (repeated)
 * 		| !this.isPossessed() || this.getBoard() == null
 * 
 * @invar The weight must be valid.
 * 		| isValidWeight(this.getWeight())
 * 
 * @author Molderez Tom, 2de bach Ingenieurswetenschappen Computerwetenschappen-Elektrotechniek and Reniers Jorn, 2de bach Ingenieurswetenschappen Werktuigkunde-Computerwetenschappen
 * @version 4.0
 */
public class RepairKit extends EnergyItem implements UsableWithBoard {

//Constructor ----------------------------------------------------------------------------------------------------------	
	
	/**
	 * Creates a new repairKit that has a certain amount of energy and weight.
	 * 
	 * @param weight The weight of the repairKit.
	 * @param energyLimit The energyLimit of the new repairKit.
	 * @param energyLevel The energyLevel of the new repairKit.
	 * @param board The board of the repairKit.
	 * @param position The position of the repairKit.
	 * @pre The energyLimit must be possible. (repeated)
	 * 		| isPossibleEnergyLimit(energyLimit)
	 * @pre The energyLevel must be positive and smaller than or equal to energyLimit (repeated)
	 * 		| (new this).canHaveAsEnergyLevel(energyLevel)
	 * @effect The energyLimit, the energyLevel, the board and position of this item are set to the given values.
	 * 		| super(energyLimit, energyLevel, board, position)
	 * @post The weight is set to the given value if valid otherwise the default value is set.
	 * 		| if !isValidWeigh(weight)
	 * 		| 	then (new this).getWeight() == DefaultConstants.REPAIR_KIT_WEIGHT
	 * 		| else
	 * 		| 	(new this).getWeigh() == weight
	 * @post
	 * 		| (new this).isPossessed() == false
	 * @throws IllegalArgumentException (repeated)
	 * 		Position is null
	 * 		| position== null
	 * @throws IllegalArgumentException (repeated)
	 * 		When the board is not null and can not contain this item.
	 * 		| board != null && !board.canHaveAsItem(new this)
	 */
	public RepairKit(PhysicalAmount weight, PhysicalAmount energyLimit, PhysicalAmount energyLevel,
			Board board, Position position) throws IllegalArgumentException {
		super(energyLimit, energyLevel, board, position);
		if(!isValidWeight(weight))
			weight = DefaultConstants.REPAIR_KIT_WEIGHT;
		this.weight = weight;
		this.setPossessed(false);
	}

	/**
	 * Creates a repairKit with default energyLimit.
	 * 
	 * @param weight The weight of the repairKit.
	 * @param energyLevel The energyLevel of the repairKit.
	 * @param board The board of the repairKit.
	 * @param position The Position of the repairKit.
	 * @pre The energyLevel must be positive and smaller than or equal to energyLimit (repeated)
	 * 		| (new this).canHaveAsEnergyLevel(energyLevel)
	 * @effect Creates a repairKit with default energyLimit.
	 * 		| this(weight, DefaultConstants.REPAIR_KIT_ENERGY_LIMIT, energyLevel, board, position)
	 * @throws IllegalArgumentException (repeated)
	 * 		Position is null
	 * 		| position== null
	 * @throws IllegalArgumentException (repeated)
	 * 		When the board is not null and can not contain this item.
	 * 		| board != null && !board.canHaveAsItem(new this)
	 */
	public RepairKit(PhysicalAmount weight, PhysicalAmount energyLevel, Board board, Position position) 
			throws IllegalArgumentException {
		this(weight, DefaultConstants.REPAIR_KIT_UPPER_ENERGY_LIMIT, energyLevel, board, position);
	}
	
	/**
	 * Creates a repairKit with default energyLimit, no board and located at the origin.
	 * 
	 * @param weight The weight of the repairKit.
	 * @param energyLevel The energyLevel of the repairKit.
	 * @pre The energyLevel must be positive and smaller than or equal to energyLimit (repeated)
	 * 		| (new this).canHaveAsEnergyLevel(energyLevel)
	 * @effect Creates a repairKit with default energyLimit, no board and located at the origin.
	 * 		| this(weight, energyLevel, null, Position.Origin)
	 */
	public RepairKit(PhysicalAmount weight, PhysicalAmount energyLevel) throws IllegalArgumentException {
		this(weight, energyLevel, null, Position.ORIGIN);
	}
	
	/**
	 * Creates a repairKit with default energyLimit, no board and located at the origin.
	 * 
	 * @param weight The weight of the repairKit.
	 * @effect Creates a repairKit with default energyLimit and energyLevel, no board and located at the origin.
	 * 		| this(weight, PhysicalAmountFactory.get().create(1000D, Unit.WATTSECOND), null, Position.Origin)
	 */
	public RepairKit(PhysicalAmount weight) throws IllegalArgumentException {
		this(weight, PhysicalAmountFactory.get().create(1000D, Unit.WATTSECOND), null, Position.ORIGIN);
	}
	
	/**
	 * Creates a default repairKit.
	 * 
	 * @effect Creates a default repairKit with default weight, energyLimit and energyLevel. The repairKit
	 * 			has no board and is located at the origin.
	 * 		| this(DefaultConstants.REPAIR_KIT_WEIGHT, 
	 *		|	EnergyAmountFactory.getInstance().newEnergyAmount(1000D, EnergyUnit.WATTSECOND))
	 */
	public RepairKit() {
		this(DefaultConstants.REPAIR_KIT_WEIGHT, 
				PhysicalAmountFactory.get().create(1000D, Unit.WATTSECOND));
	}
	
//energy ----------------------------------------------------------------------------------------------------------
	
	/**
	 * Controls if this can have the given energyLimit.
	 * 
	 * @param energyLimit The energyLimit to be controlled.
	 * @return False if the energyLimit is null. (repeated)
	 * 		| if energyLimit == null
	 * 		|	then result == false
	 * @return False if the units can not convert to Unit.WATTSECOND (repeated)
	 * 		| if !energyLimit.getUnit().canConvertTo(Unit.WATTSECOND)
	 * 		|	then result == false
	 *@return False if the energyLimit is negative or zero. (repeated)
	 * 		| if energyLimit.getAmount() <= 0
	 * 		|	then result == false
	 * @return False if the energyLimit is greater than the upper limit
	 * 			for repairKits.
	 * 		| if energyLimit.compareTo(DefaultConstants.REPAIR_KIT_UPPER_ENERGY_LIMIT) == 1
	 * 		|	then result == false
	 */
	@Override
	@Raw
	public boolean isPossibleEnergyLimit(PhysicalAmount energyLimit) {
		return super.isPossibleEnergyLimit(energyLimit) && 
				energyLimit.compareTo(DefaultConstants.REPAIR_KIT_UPPER_ENERGY_LIMIT) != 1;
		
	}
	
//Weight ----------------------------------------------------------------------------------------------------------	
	
	/**
	 * Returns the weight of this repairKit.
	 */
	@Override 
	@Basic @Raw @Immutable
	public PhysicalAmount getWeight() {
		return this.weight;
	}
	
	/**
	 * Compares the item with weight to this one.
	 * 
	 * @param item The item with which this must be compared.
	 * @return (repeated)
	 * 		| result == this.getWeight().compareTo(item.getWeight())
	 * @throws IllegalArgumentException (repeated)
	 * 		| usable == null
	 */
	@Override
	public int compareTo(ItemWithWeight item) throws IllegalArgumentException{
		if(item == null)
			throw new IllegalArgumentException("Cannot compare to null");
		return this.getWeight().compareTo(item.getWeight());
	}
	
	/**
	 * Checks the weight.
	 * 
	 * @param weight The weight to be checked.
	 * @return False if the weight is null
	 * 		| if weight == null
	 * 		|	then result == false
	 * @return False if the unit of the weight can not converted to Unit.GRAM
	 * 		| if !weight.getUnit().canConvertTo(Unit.GRAM)
	 * 		|	then result == false
	 * @return False if the weight amount is negative.
	 * 		| if weight.getAmount() < 0
	 * 		|	then result == false.
	 */
	@Raw
	public static boolean isValidWeight(PhysicalAmount weight){
		if(weight == null)
			return false;
		else if(!weight.getUnit().canConvertTo(Unit.GRAM))
			return false;
		else if(weight.getAmount() < 0)
			return false;
		return true;
	}
	
	/**
	 * The weight of this repairKit.
	 */
	private final PhysicalAmount weight;
	
//Position ----------------------------------------------------------------------------------------------------------
	
	/**
	 * Checks whether this repairKit can share a position with the given boardItem.
	 * 
	 * @param boardItem The item which is at the same position as this repairKit.
	 * @return False if boardItem is null. (repeated)
	 * 		| if boardItem == null
	 * 		|	then result == false
	 * @return True if boardItem is the same as this. (repeated)
	 * 		| if positionItem == this
	 * 		| 	then result == true
	 * @return False if boardItem is a Wall.
	 * 		| if Wall.class.isInstance(boardItem)
	 *		|	then result == false
	 * @return True if boardItem is a battery.
	 * 		| if Battery.class.isInstance(boardItem)
	 *		|	then result == true
	 * @return True if boardItem is a repairKit.
	 * 		| if RepairKit.class.isInstance(boardItem)
	 *		|	then result == true
	 * @return True if boardItem is a robot.
	 * 		| if Robot.class.isInstance(boardItem)
	 *		|	then result == true
	 * @return When another type of boardItem is created, in the future, he is responsible for handling this.
	 * 		| result == boardItem.canHaveItemOnSamePosition(this)
	 */
	@Override
	public boolean canHaveItemOnSamePosition(BoardItem boardItem) {
		if(boardItem == null)
			return false;
		else if(Wall.class.isInstance(boardItem))
			return false;
		else if(Battery.class.isInstance(boardItem))
			return true;
		else if(RepairKit.class.isInstance(boardItem))
			return true;
		else if(Robot.class.isInstance(boardItem))
			return true;
		else
			return boardItem.canHaveItemOnSamePosition(this);
	}

//Shoot ----------------------------------------------------------------------------------------------------------
	
	/**
	 * If this is been shot, the energyLevel increases by some amount. If the energyLevel is greater
	 * than the limit, the limit will be set.
	 * 
	 * @pre
	 * 		| this.getBoard() != null
	 * @pre
	 * 		| !this.isTerminated()
	 * @effect If this can have the energyLevel, added by some amount, than the energyLevel will be set to that value
	 * 			Otherwise the energyLevel will be set to the energyLimit.
	 * 		| if this.canHaveAsEnergyLevel(this.getEnergyLevel().add(DefaultConstants.BATTERY_BEEN_HIT)
	 * 		|	then this.setEnergyLevel(this.getEnergyLevel().add(DefaultConstants.BATTERY_BEEN_HIT))
	 * 		| else
	 * 		|	then this.setEnergyLevel(this.getEnergyLimit())
	 * 		
	 */
	@Override
	public void beenShot() {
		assert this.getBoard() != null;
		assert !this.isTerminated();
		PhysicalAmount energyLevel = this.getEnergyLevel().add(DefaultConstants.REPAIR_KIT_BEEN_HIT);
		if(this.canHaveAsEnergyLevel(energyLevel))
			this.setEnergyLevel(energyLevel);
		else
			this.setEnergyLevel(this.getEnergyLimit());
	}

//Usable ----------------------------------------------------------------------------------------------------------	
	
	
	
	/**
	 * Let the robot pick up this repairKit, if possible.
	 * 
	 * @param robot The robot that should pick up this repairKit.
	 * @pre (repeated)
	 * 		| robot != null
	 * @pre  (repeated)
	 * 		| robot.canPickUp(this)
	 * @pre (repeated)
	 * 		| !isTerminated()
	 * @pre (repeated)
	 * 		| !isPossessed()
	 * @effect (repeated)
	 * 		| robot.getBoard().removeAsItem(this)
	 */
	 @Override
	public void isPickedUp(Robot robot) {
		assert robot != null;
		assert robot.canPickUp(this);
		assert !isTerminated();
		assert !isPossessed();
		robot.getBoard().removeAsItem(this);
	}
	
	/**
	 * Let the robot drop this repairKit, if possible.
	 *  
	 * @param robot The robot that should drop this repairKit.
	 * @pre (repeated)
	 * 		| robot != null
	 * @pre (repeated)
	 * 		| robot.canDrop(this)
	 * @pre (repeated)
	 * 		| !isTerminated()
	 * @pre (repeated)
	 * 		| !isPossessed()
	 * @effect (repeated)
	 * 		| if robot.getBoard() != null
	 * 		|	then robot.getBoard().addAsItem(this)
	 * @effect (repeated)
	 * 		| if robot.getBoard() == null
	 * 		|	then this.setBoard(null)
	 * @post (repeated)
	 * 		| (new this).getPosition() == robot.getPosition()
	 * 
	 * @note The robot will leave this method in a raw state because this battery will have a board and
	 * a position, and the robot will possess this repairKit.
	 */
	@Override
	@Raw 
	public void isDropped(Robot robot) {
		assert robot != null;
		assert robot.canDrop(this);
		assert !isTerminated();
		assert !isPossessed();
		this.setPosition(robot.getPosition());
		if(robot.getBoard()!=null)
			robot.getBoard().addAsItem(this);
		else
			this.setBoard(null);
	}
	
	/**
	 * The energy of this repairKit is transfered to the energyLimit of the robot, using a conversing factor if the robot can contain 
	 * the EnergyLimit. Else the robot will have the default energyLimit.
	 * 
	 * @param robot The robot that will use this repairKit.
	 * @pre (repeated)
	 * 		| robot != null
	 * @pre (repeated)
	 * 		| robot.canUse(this)
	 * @pre (repeated)
	 * 		| !isTerminated()
	 * @post If the robot can have as energyLimit his energyLimit, added by the energyLevel of this repairKit multiplied by a conversing factor, then the robot
	 * 		will have that energyLimit and this will have zero as energyLevel. Else the robot will have the default energyLimit and this will have a 
	 * 		energyLevel subtracted by the difference of the old energyLimit of the robot and the default one, divided by the conversing factor.
	 * 		| if robot.canHaveAsEnergyLimit(robot.getEnergyLimit().add(this.getEnergyLevel().multiply(DefaultConstants.REPAIR_KIT_FACTOR)))
	 * 		|	then (new robot).getEnergyLimit().equals(robot.getEnergyLimit().add(this.getEnergyLevel().multiply(DefaultConstants.REPAIR_KIT_FACTOR)))
	 * 		|		 (new this).getEnergyLevel().equals(EnergyAmount.ZERO)
	 * 		| else
	 * 		|	(new robot).getEnergyLimit().equals(DefaultConstants.ROBOT_ENERGY_LIMIT)
	 * 		|	(new this).getEnergyLevel().equals(this.getEnergyLevel().subtract(
	 * 		|			DefaultConstants.ROBOT_ENERGY_LIMIT.subtract((old robot).getEnergyLimit()).division(DefaultConstants.REPAIR_KIT_FACTOR)))
	 */
	@Override
	public void isUsed(Robot robot) {
		assert robot != null;
		assert robot.canUse(this);
		assert !this.isTerminated();
		PhysicalAmount energyLimit = robot.getEnergyLimit().add(this.getEnergyLevel().multiply(DefaultConstants.REPAIR_KIT_FACTOR));
		if(robot.canHaveAsEnergyLimit(energyLimit)) {
			robot.setEnergyLimit(energyLimit);
			this.setEnergyLevel(PhysicalAmount.ENERGY_ZERO);
		}
		else {
			this.setEnergyLevel(this.getEnergyLevel().subtract(
					 DefaultConstants.ROBOT_ENERGY_LIMIT.subtract(robot.getEnergyLimit()).division(DefaultConstants.REPAIR_KIT_FACTOR))); // can never become negative
			robot.setEnergyLimit(DefaultConstants.ROBOT_ENERGY_LIMIT);
		}
		
	}
	
	/**
	 * Returns whether this repairKit is possessed by a robot or not.
	 */
	@Override
	@Basic @Raw 
	public boolean isPossessed(){
		return possessed;
	}
	
	/**
	 * Sets whether this repairKit is possessed by a robot or not.
	 * 
	 * @param value The value to be set.
	 * @post (repeated)
	 * 		| (new this).isPossessed() == value
	 */
	@Override
	public void setPossessed(boolean value){
		possessed = value;
	}
	
	private boolean possessed;
	
//Object ----------------------------------------------------------------------------------------------------------	
	
	/**
	 * Returns a textual specification of this repairKit.
	 * 
	 * @return A textual specification of this repairKit.
	 * 		| result == super.toString() + ", [Weight: " + this.getWeight() + "]"
	 */
	@Override
	public String toString() {
		return super.toString() + ", [Weight: " + this.getWeight() + "]";
	}
	
//LegalStatus ----------------------------------------------------------------------------------------------------------
	
	/**
	 * Controls if the repairKit satisfies all his class invariants.
	 * 
	 * @return False if the board is not valid. (repeated)
	 * 		| if !hasProperBoard()
	 * 		|	then result == false
	 * @return False if the position is not legal. (repeated)
	 * 		| if !canHaveAsPosition(this.getPosition())
	 * 		|	then result == false
	 * @return False if the energyLevel is not valid. (repeated)
	 * 		| if !canHaveAsEnergyLevel(this.getEnergyLevel()
	 * 		|	then result == false
	 * @return False if the energyLimit is not valid. (repeated)
	 * 		| if !canHaveAsEnergyLimit(this.getEnergyLimit())
	 * 		|	then result == false
	 * 
	 * @return False if the weight is not valid.
	 * 		| if !RepairKit.isValidWeight(this.getWeight())
	 * 		|	then result == false
	 * @return False if the repairKit is possessed and has a board.
	 * 		| if this.isPossessed() && this.getBoard() != null
	 * 		|	then result == false
	 */	
	@Override
	public boolean legalStatus() {
		return super.legalStatus() && RepairKit.isValidWeight(this.getWeight()) && (!this.isPossessed() || this.getBoard() == null);
	}
}
